⚠️ 学习声明:本文档基于 Claude Code 2.1.88 源码分析整理,仅供个人学习研究使用,不做任何商业用途。
LSP(Language Server Protocol)让 Claude 获得”编译器视角”——实时错误、代码导航、类型信息。
一、系统定位
为什么需要 LSP?
| 能力 |
没有 LSP |
有 LSP |
| 错误检测 |
靠 Claude 静态分析或运行 build |
实时 publishDiagnostics 推送 |
| 代码跳转 |
文本搜索(grep) |
textDocument/definition 语义查找 |
| 类型信息 |
无 |
textDocument/hover |
| 补全 |
无 |
textDocument/completion |
配置来源
关键设计:LSP 服务器只能通过插件(Plugin)配置,不能在 user/project settings 中直接设置。
config.ts: getAllLspServers() → loadAllPluginsCacheOnly() → 每个插件的 getPluginLspServers() → 合并所有插件的 LSP 配置(后载入插件胜出)
|
二、核心文件地图
src/services/lsp/ ├── LSPClient.ts # 底层 LSP 客户端(vscode-jsonrpc over stdio) ├── LSPServerInstance.ts # 单个 LSP 服务器实例管理(状态机) ├── LSPServerManager.ts # 多服务器路由管理器(按文件扩展名路由) ├── LSPDiagnosticRegistry.ts # 诊断信息异步注册表(LRU + 去重) ├── passiveFeedback.ts # Diagnostics → Attachment 转换器 ├── config.ts # 从插件加载服务器配置 └── manager.ts # 导出统一的 manager 实例
|
三、LSPClient 底层通信(LSPClient.ts)
传输协议
LSP Server Process ↕ stdio (stdin/stdout) ↕ vscode-jsonrpc MessageConnection LSPClient (CC 内)
|
接口定义
type LSPClient = { readonly capabilities: ServerCapabilities | undefined readonly isInitialized: boolean start(command, args, options): Promise<void> initialize(params): Promise<InitializeResult> sendRequest<T>(method, params): Promise<T> sendNotification(method, params): Promise<void> onNotification(method, handler): void onRequest(method, handler): void stop(): Promise<void> }
|
懒加载 + 崩溃恢复
createLSPClient(serverName, onCrash?) { let pendingHandlers = [] }
|
四、服务器实例管理(LSPServerInstance.ts)
状态机
stopped ──start()──→ starting ──success──→ running ↑ │ └──stop()────────── stopping ←──stop()───┘ │ error (on failure) │ starting (on retry)
|
瞬态错误重试
const LSP_ERROR_CONTENT_MODIFIED = -32801 const MAX_RETRIES_FOR_TRANSIENT_ERRORS = 3 const RETRY_BASE_DELAY_MS = 500
|
rust-analyzer 等 LSP 服务器在项目初次索引期间会返回 -32801,CC 自动重试。
五、多服务器路由(LSPServerManager.ts)
路由策略:按文件扩展名
const extensionMap: Map<string, string[]> = new Map()
getServerForFile(filePath: string): LSPServerInstance | undefined { const ext = path.extname(filePath) const serverNames = extensionMap.get(ext) return serverNames?.[0] ? servers.get(serverNames[0]) : undefined }
|
文件同步协议
await manager.openFile(filePath, content)
await manager.changeFile(filePath, content)
await manager.saveFile(filePath)
await manager.closeFile(filePath)
|
打开文件追踪:openedFiles: Map<string, string> 记录 URI → serverName,防止重复 didOpen,节省服务器资源。
六、诊断信息异步交付(LSPDiagnosticRegistry.ts)
核心问题
LSP 诊断是服务器主动推送(textDocument/publishDiagnostics),而 CC 的工具结果是请求-响应模式。需要一个”异步缓冲区”桥接两者。
注册表设计
const pendingDiagnostics = new Map<string, PendingLSPDiagnostic>()
const deliveredDiagnostics = new LRUCache<string, Set<string>>({ max: MAX_DELIVERED_FILES, })
|
体量限制
const MAX_DIAGNOSTICS_PER_FILE = 10 const MAX_TOTAL_DIAGNOSTICS = 30
|
交付流程
LSP Server → publishDiagnostics 推送 ↓ registerPendingLSPDiagnostic() → 存入 pendingDiagnostics ↓ 下一轮查询开始 ↓ checkForLSPDiagnostics() → 取出待交付诊断 ↓ getLSPDiagnosticAttachments() → 转为 Attachment[] ↓ getAttachments() → 自动注入到对话上下文
|
七、诊断格式转换(passiveFeedback.ts)
LSP Severity → Claude Severity
function mapLSPSeverity(lspSeverity: number | undefined) : 'Error' | 'Warning' | 'Info' | 'Hint'
|
URI 处理
八、插件 LSP 集成(lspPluginIntegration.ts)
插件提供 LSP 配置的格式
type LspServerConfig = { command: string args?: string[] extensionToLanguage: Record<string, string> transport?: 'stdio' | 'socket' env?: Record<string, string> initializationOptions?: unknown settings?: unknown workspaceFolder?: string }
|
并行加载所有插件的 LSP 配置
const results = await Promise.all( plugins.map(plugin => getPluginLspServers(plugin, errors)) )
|
九、与工具系统的集成
工具调用时的文件同步
Claude 调用 FileReadTool 读取 src/main.ts ↓ FileReadTool 执行后通知 LSPServerManager ↓ manager.openFile('src/main.ts', content) // 触发 didOpen ↓ LSP 服务器开始分析该文件 ↓ 几百毫秒后 publishDiagnostics 推送 ↓ 下一轮对话中自动附加诊断信息
|
Claude Edit 时的文件同步
Claude 调用 FileEditTool 修改代码 ↓ manager.changeFile() + manager.saveFile() ↓ LSP 服务器实时更新分析 ↓ 新的类型错误/lint 错误自动推送给 Claude
|
十、LSP 推荐系统(lspRecommendation.ts)
当用户没有配置 LSP 服务器时,CC 可以根据项目语言推荐安装:
检测项目语言(package.json → TypeScript, *.py → Python, ...) ↓ lspRecommendation.ts 生成安装建议 ↓ 通过 CLAUDE.md 或初始化消息展示给用户
|
十一、关键设计决策
| 决策 |
原因 |
| 只通过插件配置 LSP |
避免全局 settings 因 LSP 服务器崩溃影响核心功能 |
| 异步诊断注册表 |
LSP 推送是服务器主动行为,与 CC 请求-响应模型解耦 |
| LRU 去重缓存 |
长会话中同一文件反复诊断,避免重复投喂上下文 |
| 瞬态错误重试(指数退避) |
rust-analyzer 等在索引期间会短暂返回 -32801 |
| 按扩展名路由 |
简单高效,支持多语言并存(TypeScript + Python + Rust) |
| URI 转换降级 |
LSP 服务器偶发 malformed URI,不能因此崩溃整个诊断流程 |
涉及源文件